<?php
/**
 *
 * @todo description of this class
 *
 * @author Alton Crossley <crossleyframework@nogahidebootstrap.com>
 * @package Crossley Framework
 *
 * @copyright Copyright (c) 2003-2009, Nogaide BootStrap INC. All rights reserved.
 * @license BSD http://opensource.org/licenses/bsd-license.php
 * @version $Id:$
 *
 */

/**
 * @namespace
 */
namespace X\Controller;
use X\Exception;

use \X\Controller;
use \X\Controller\Event;
use \X\Controller\Response;
use \X\Controller\Event\Request;
use \X\Controller\Response\Renderer;

use \X\DataStructure\String;
use \X\DataStructure\ArrayUtil;

/**
 *
 * This is meant to be a focused front controler serving up
 * class based content or static file content
 *
 *
 */
/**
 * basic front Broker
 *
 */
class Front implements Controller
{
    /**
     * incoming event
     *
     * @var X\Controller\Event
     */
    public $Event;
    /**
     * response singelton
     *
     * @var X\Controller\Response
     */
    public $Response;

    /**
     * path to default Controller and handler classes
     *
     * @var string
     */
    protected $_ControllerPath = '../application/';

    /**
     * Namespace to prepend to controllers per convention
     * usually used as a subordinate to application namesapce
     * @var string
     */
    protected $_ControllerNamespace = 'Controller\\';

    /**
     * Namespace to prepend to render classes per convention
     * usually used as a subordinate to application namesapce
     * @var string
     */
    protected $_RenderNamespace = 'Render\\';
    /**
     * key value map to module paths
     *
     * @var array
     */
    protected $_ModulePaths = array();
    /**
     * map first step to a library controller
     * ex: admin => Relief
     *
     * @var array
     */
    protected $_ClassMap = array();
    /**
     * array of callable error handlers
     * each error is given an index, if assigned it is caleld like:
     * $this->_ExceptionHandlers['not_found']($exception, $currentresponse);
     *
     * @var array
     */
    protected $_ExceptionHandlers = array();
    /**
     * headers to be sent to the client on the current request
     *
     * @var array
     */
    protected $_Headers = array();
    /**
     * allow printing of errors and messages
     *
     * @var bool
     */
    protected $_Debug = false;
    /**
     * trace of controller debug information
     *
     * @var \X\Logger
     */
    protected $_Logger;
    /**
     * global front controller registry
     * for use in handlers
     *
     * @var array
     */
    static protected $_Registry = array();
    /**
     * singelton front controller instance
     *
     * @var X_Controller_Front
     */
    static protected $_Instance;
    /**
     * namespace with a trailing backslash
     * @var string
     */
    static protected $_ApplicationNamespace;
    /**
     * register a value in the Broker registry
     *
     * @param string $sKey
     * @param unknown_type $xValue
     */
    static public function registerValue($sKey, $xValue)
    {
        self::$_Registry[$sKey] = $xValue;
    }
    /**
     * return all the registed values
     *
     * @return array
     */
    static public function getRegisteredValues()
    {
        return self::$_Registry;
    }
    /**
     * retrieve a value from the Broker registry
     *
     * @param string $sKey
     * @return unknown_type
     */
    static public function getRegistered($sKey)
    {
        if (array_key_exists($sKey, self::$_Registry))
        {
            return self::$_Registry[$sKey];
        }
        return false;
    }
    /**
     * Get or set the global application namespace
     * Namespace should be from root (leading and trailing slash enforced)
     * @example \MyApp\
     *
     * @param string $AppNamespace namespace from root
     * @return string
     */
    static public function ApplicationNamespace($AppNamespace = null)
    {
        if (!empty($AppNamespace))
        {
            // make sure we have a trailing slash
            if (\strpos($AppNamespace, '\\') === false)
            {
                self::$_ApplicationNamespace = $AppNamespace . '\\';
            }
            else
            {
                self::$_ApplicationNamespace = $AppNamespace;
            }
            // make sure we have a leading slash
            if (\substr(self::$_ApplicationNamespace, 0, 1) != '\\')
            {
                self::$_ApplicationNamespace = '\\' . self::$_ApplicationNamespace;
            }
        }

        return self::$_ApplicationNamespace;
    }
    /**
     * enable debuging mode
     * @return bool
     */
    public function isDebug($Debug = null, \X\Logger $Logger = null)
    {
        // reset debug if it is passed
        if ($Debug !== null)
        {
            $this->_Debug = (bool)$Debug;
        }
        
        // set logger if debug is true
        if ($this->_Debug)
        {
            $this->_Logger = ($Logger) ? $Logger : \X\Logger\Memory::getInstance('router');
        }
        
        // return the value of logger
        return $this->_Debug;
    }
    /**
     * log a message
     * @param unknown_type $Message
     * @param unknown_type $Type
     */
    protected function log($Message, $Type = 'general')
    {
        if ($this->_Debug && $this->_Logger) $this->_Logger->logMessage($Message, $Type);
    }
    /**
     * Used for internal calls to the front controller
     *
     * @param string $RelativeUrl
     * @param array $EventData
     */
    static public function callLoopback($RelativeUrl, $EventData = array())
    {
        if ($EventData instanceof Event)
        {
            $Event = $EventData;
            $EventData = $Event->getData();
        }
        else
        {
            $Event = new Event(false);
            $Event->setData($EventData);
        }
           
        $Event->setUri($RelativeUrl);
        return self::getResponse($Event);
    }
    /**
     * get the singelton instance
     *
     * @return X_Broker
     */
    static public function getInstance()
    {
        $ClassName = __CLASS__;
        if (self::$_Instance instanceof $ClassName)
        {
            return self::$_Instance;
        }
        self::$_Instance = new Front();
        return self::$_Instance;
    }
    /**
     * standard constructor
     */
    function __construct()
    {
        $this->Response = new Response\Standard();
        $this->getEvent();
    }
    /**
     * adds a error string to the error singelton
     *
     * @param string $sError
     */
    static public function addError($Error)
    {
        $this->_ErrorHandler->handle($Error);
        $this->log($Error, 'Error');
        return $this;
    }
    /**
     * handle input event
     * if no event is given, it will be obtained from the
     * static getClientParameters method
     *
     * @param Array
     * @return String|Response|false printable output, false on failure
     */
    public function respond(Event $Event = null, Response $Response = null)
    {
        if ($Event)
        {
            $this->Event = $Event;
        }
        else
        {
            $this->getEvent();
        }

        // modify the directory path if we are loading a module
        $this->_mapModulePath();

        // Identify the first path step
        $StepName = $this->Event->getStep();
        // get the global applicaion namespace
        $AppNamespace = Front::ApplicationNamespace();
        // default controller path
        $ControllerPath = $this->_ControllerPath;
        
        $ControllerClassName = $this->_expandClassName(String::toCapWords($StepName));
        
        try
        {
            if (!$this->_Debug) ob_start();
            $Controller = $this->_getApplicationRouter($ControllerClassName);
            $this->log(__METHOD__. '(' .__LINE__. ") ControllerClassName: {$ControllerClassName}", 'routing');
            
            // get the response to the the event
            $ControllerResponse = $Controller->respond($this->Event, $this->Response);
            
            // handle a string response
            if ($ControllerResponse instanceof Response)
            {
                // allow for rendering logic
                $Output = $this->_render($ControllerResponse);
            }
            else
            {
                $ControllerClassName = get_class( $Controller );
                throw new Exception("Controller $ControllerClassName did not return an object of type \X\Controller\Response");
            }
            
            
            
            // skim any junk that may have happened here
            if (!$this->_Debug) $Dross = ob_get_clean();
            
            // return output
            return $Output;
            
        }
        catch (\Exception $e)
        {
            $ExceptionCode = $e->getCode();
            // check for and call a handler
            if (\array_key_exists($ExceptionCode, $this->_ExceptionHandlers) && \is_callable($this->_ExceptionHandlers[$ExceptionCode]))
            {
                $this->_ExceptionHandlers[$ExceptionCode]($e, $this->Response);
                $this->log(__METHOD__. '(' .__LINE__. ")Handled Exception: {$e->getMessage()}", 'routing');
            }
            else
            {
                // save the error
                $this->Response->addError($e->getMessage() . "\n" . $e->getTraceAsString());
                $this->log(__METHOD__. '(' .__LINE__. ")Exception: {$e->getMessage()}", 'routing');
            }
        }
        
        return false;
    }

    /**
     * Map controller path when there is a module to be loaded
     * also change the namespace if it is a module
     *
     */
    protected function _mapModulePath()
    {
        // check to see if the path step is a module
        if (\array_key_exists(0, $this->Event->getPath()))
        {
            $ModuleKey = $this->Event->getPath();
            $ModuleKey = $ModuleKey[0];
            if (\array_key_exists((string)$ModuleKey, $this->_ModulePaths))
            {
                // set module path
                $this->_ControllerPath = $this->_ModulePaths[$StepName];
                // pull the given path step into the history
                $this->Event->getStep();
            }
        }

        self::registerValue('controller_path', $this->_ControllerPath);
    }

    /**
     * create the full controller class name
     * @param string $ClassKey
     * @param string $AppNamespace
     * @return string
     */
    protected function _expandClassName($ClassKey)
    {
        if (!empty($ClassKey) && \array_key_exists($ClassKey, $this->_ClassMap))
        {
            return $this->_ClassMap[$ClassKey];
        }
        else if ($ClassKey)
        {
            return Front::ApplicationNamespace() . $this->_ControllerNamespace . $ClassKey;
        }
        
        return Front::ApplicationNamespace() . $this->_ControllerNamespace . 'Index';
        
    }

    /**
     * Create an instance of a Ruter
     *
     * @param <type> $ControllerClassName
     * @return Router\Router
     */
    protected function _getApplicationRouter($ControllerClassName)
    {
        if ($this->_loadClass($ControllerClassName, $this->_ControllerPath))
        {
            $Controller = new $ControllerClassName();
            
            if (!($Controller instanceof  Controller))
            {
                throw new Exception("$ControllerClassName does not implement Controller Interface");
            }

        }
        else if ($this->_loadClass('GlobalController', $this->_ControllerPath))
        {
            $ControllerClassName = 'GlobalController';
            
            $Controller = new GlobalController();
            if ($Controller instanceof  Controller)
            {
                // reset path for generic usage
                $this->Event->resetPath();
            }
            else
            {
                throw new Exception("GlobalController does not implement Controller Interface");
            }
        }
        else
        {
            $ControllerClassName = 'Router';

            // reset path for generic usage
            $this->Event->resetPath();
            // get an instance of the default router controller
            $Controller = new Router();
        }
        
        if (method_exists($Controller, 'isDebug')) $Controller->isDebug($this->_Debug);

        // default the name to the same as the controller
        $this->Response->setRenderName(basename($ControllerClassName));

        return $Controller;
    }
    /**
     * loads and runs the renderer specified by the response
     * the Renderer will often be the internal HTML or JSON renderer
     *
     * @param Response $Response
     * @return string
     */
    protected function _render(Response $Response)
    {
        // set default case - if all else fails
        $RendererObject = false;
        
        if ($this->_RenderNamespace)
        {
            // try to load the renderer that is specified by the response
            $RendererClassName = self::ApplicationNamespace() . $this->_RenderNamespace . $Response->getRenderName();
            $RendererObject = $this->_getRenderer($RendererClassName);
        }
        
        // try to use the renderer
        if (!$RendererObject || !($RendererObject instanceof Renderer))
        {
            $RendererClassName = '\\X\\Controller\\Response\\Renderer\\Render' . ucwords( $Response->OutputMethod() );
            $RendererObject  = $this->_getRenderer($RendererClassName);

            if (!$RendererObject || !($RendererObject instanceof Renderer))
            {
                throw new Exception("unable to load {$this->_ControllerPath}{$RendererClassName} or the standard renderer ({$RendererClassName})");
            }
        }
        
        // renderer should not output
        if (!($RendererObject instanceof Renderer)) throw new \X\Controller\AbsentException('Unable to resolve Render Class');
        
        $Output = $RendererObject->render($Response);
        $this->log(__METHOD__. '(' .__LINE__. ") RendererClassName: {$RendererClassName}", 'routing');
        
        return $Output;
    }
    /**
     * attempts to load the render class and returns it or return false on failure
     * throws an exception if the class is loaded and is not an instance of Renderer
     * @param string $RenderClassName
     * @throws Exception
     * @returns Renderer or false on failure
     */
    protected function _getRenderer($RendererClassName)
    {
        if ($this->_loadClass($RendererClassName, $this->_ControllerPath))
        {
            $RendererObject = new $RendererClassName();
            if (!($RendererObject instanceof  Renderer))
            {
                throw new Exception("$RendererClassName does not implement Renderer Interface");
            }
            return $RendererObject;
        }
        return false;
    }

    /**
     * set a first step to map to a library controller
     *
     * @param string $StepName
     * @param string $ClassName
     * @return Front
     */
    public function addClassMap($StepName, $ClassName)
    {
        if (empty($StepName) && empty($ClassName))
        {
            return $this;
        }

        $this->_ClassMap[(string)$StepName] = (string)$ClassName;
        return $this;
    }
    
    /**
     * assign an error handler controler
     *
     * @param Controller $ErrorHandler
     */
    public function setExceptionHandler($ErrorKey, $Handler)
    {
        if (!\is_callable($Handler))
        {
            throw new Exception('Error Handler must be callable');
        }
        
        $this->_ExceptionHandlers[$ErrorKey] = $oErrorHandler;
        return $this;
    }
    /**
     *
     * @param string $ErrorKey
     * @return bool
     */
    public function isExceptionHandler($ErrorKey)
    {
        return \array_key_exists($ErrorKey, $this->_ExceptionHandlers);
    }
    /**
     * restrict the path for loading controllers
     * @param string $Path
     * @param bool $UseIncludePath adds the path to the include path instead of restricting the path
     * @return bool truen on success
     */
    public function setControllerPath($Path, $UseIncludePath = false)
    {
        if (empty($Path))
        {
            trigger_error("path is empty (if you are using realpath it may not exist)", E_USER_WARNING);
            return false;
        }

        $RealPath = realpath($Path);
        if ($UseIncludePath)
        {
            \Loader::addIncludePath($RealPath, true);
        }
        else if ($RealPath)
        {
            $this->_ControllerPath = $RealPath . DIRECTORY_SEPARATOR;
        }
        else
        {
            throw new Controller\Exception($Path . ' does not exist');
        }

        return $this;
    }
    /**
     * returns the current event
     * @return Event
     */
    public function &getEvent()
    {
        // check to see if we have an Event Instance
        if (! isset($this->Event))
        {
            // build one if we dont
            $this->Event = new Request();
            // Broker Event automatically scraps the path and GP data.
        }
        return $this->Event;
    }
    /**
     * simple load class wrapper
     *
     * @param string $ControllerClassName
     * @param string $Path
     */
    protected function _loadClass($ControllerClassName, $Path = null)
    {
        \X\Loader::loadClass($ControllerClassName, $Path);
        return \class_exists($ControllerClassName);
    }
}